package com.sp.business.normalization.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.apache.commons.lang.NullArgumentException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sp.generated.xml.firemon.NATRuleCollection;
import com.sp.generated.xml.firemon.Policy;
import com.sp.generated.xml.firemon.PolicyCollection;
import com.sp.generated.xml.firemon.SecurityRuleCollection;

public class PolicyKeyGenerator
{
  private static final Logger logger = LoggerFactory.getLogger(PolicyKeyGenerator.class);
  private SecurityRuleKeyGenerator securityRuleKeyGenerator;
  private NatRuleKeyGenerator natRuleKeyGenerator;

  public PolicyKeyGenerator(SecurityRuleKeyGenerator keyGenerator, NatRuleKeyGenerator natRuleKeyGenerator)
  {
    this.securityRuleKeyGenerator = keyGenerator;
    this.natRuleKeyGenerator = natRuleKeyGenerator;
  }

  public void generatePolicyKeys(PolicyCollection previousPolicies, PolicyCollection currentPolicies)
  {
    if (currentPolicies == null)
      throw new NullArgumentException("currentPolicies");

    // key statistics
    int matchedKeyCount = 0;
    int generatedKeyCount = 0;

    long start = System.currentTimeMillis();

    List<Policy> nonMatchedPolicies = new ArrayList<Policy>(currentPolicies.getPolicies());
    List<Policy> nonMatchedPreviousPolicies;

    if (previousPolicies == null)
      nonMatchedPreviousPolicies = new ArrayList<Policy>();
    else
      nonMatchedPreviousPolicies = new ArrayList<Policy>(previousPolicies.getPolicies());

    // find exact matches first
    for (Policy policy : currentPolicies.getPolicies())
    {
      Policy previousPolicy = findPolicyExactMatch(nonMatchedPreviousPolicies, policy);

      if (previousPolicy == null)
        continue;

      copyUID(previousPolicy, policy);

      // remove the previous policy, so it doesn't get matched again
      nonMatchedPreviousPolicies.remove(previousPolicy);

      // remove the policy, so we don't try to match it again
      nonMatchedPolicies.remove(policy);

      // match security rule keys      
      securityRuleKeyGenerator.generateSecurityRuleKeys(previousPolicy.getSecurityRules(), policy.getSecurityRules());

      // match nat rule keys
      if (natRuleKeyGenerator != null)
      {
        natRuleKeyGenerator.generateNATRuleKeys(previousPolicy.getNATRules(), policy.getNATRules());
      }

      matchedKeyCount++;
    }

    // now try to match remaining, non-exact-matched policies
    for (Policy policy : nonMatchedPolicies)
    {
      Policy previousPolicy = findPolicyCloseMatch(nonMatchedPreviousPolicies, policy);
      SecurityRuleCollection securityRules = null;
      NATRuleCollection natRules = null;

      // copy or generate the guid
      copyUID(previousPolicy, policy);

      if (previousPolicy == null)
      {
        /* no close match could be found for the policy 
         * the policy is either a newly-created policy, 
         * or it has changed so significantly that 
         * it is effectively a new policy.
        */
        generatedKeyCount++;
      }
      else
      {
        // we found a close match,
        // remove the previous policy, so it doesn't get matched again
        nonMatchedPreviousPolicies.remove(previousPolicy);

        securityRules = previousPolicy.getSecurityRules();
        natRules = previousPolicy.getNATRules();

        matchedKeyCount++;
      }

      if(policy.getSecurityRules() != null)
      {
        // match security rule keys
        securityRuleKeyGenerator.generateSecurityRuleKeys(securityRules, policy.getSecurityRules());
      }

      // match nat rule keys
      if (natRuleKeyGenerator != null)
      {
        if(policy.getNATRules() != null)
        {
          natRuleKeyGenerator.generateNATRuleKeys(natRules, policy.getNATRules());
        }
      }
    }

    logger.info(String.format(
				"Matched %s and generated %s policy keys in %s ms.",
				matchedKeyCount,
        generatedKeyCount, System.currentTimeMillis() - start));
  }

  private static String copyUID(Policy previous, Policy current)
  {
    String guid = null;

    if (previous != null)
      guid = previous.getUID();

    if (StringUtils.isBlank(guid) || !NormalizationUtil.isStringUUID(guid))
      guid = String.format("{%s}", UUID.randomUUID().toString().toUpperCase());

    current.setUID(guid);

    current.setXsid("Policy" + guid.replaceAll("\\{", "").replaceAll("\\}", ""));

    return guid;
  }

  private static Policy findPolicyExactMatch(List<Policy> previousPolicies, Policy policy)
  {
    for (Policy previousPolicy : previousPolicies)
    {
      if (isPolicyExactMatch(previousPolicy, policy))
        return previousPolicy;
    }

    return null;
  }

  private static boolean isPolicyExactMatch(Policy previousPolicy, Policy policy)
  {
    // test name
    if (!KeyGeneratorBase.isExactMatch(previousPolicy.getName(), policy.getName()))
      return false;

    return true;
  }

  private static Policy findPolicyCloseMatch(List<Policy> previousPolicies, Policy policy)
  {
    int lowestEditDistance = Integer.MAX_VALUE;
    Policy bestMatch = null;

    for (Policy previousPolicy : previousPolicies)
    {
      int editDistance = getEditDistance(previousPolicy, policy);

      if (editDistance < lowestEditDistance)
      {
        lowestEditDistance = editDistance;
        bestMatch = previousPolicy;
      }
    }

    // TODO: jcoon - need to make this policy matching threshold configurable
    float maximumAllowableEditDistance = 50;

    if (lowestEditDistance > maximumAllowableEditDistance)
      return null;

    return bestMatch;
  }

  /**
   * Find the edit distance between two Policies.
   * 
   * This is the number of changes needed to change one Policy into another, 
   * where each change is a single modification (single character difference between names, etc).
   * 
   * @param previousPolicy
   * @param policy
   * @return
   */
  private static int getEditDistance(Policy previousPolicy, Policy policy)
  {
    // name
    int editDistance = KeyGeneratorBase.getEditDistance(previousPolicy.getName(), policy.getName());

    return editDistance;
  }
}
